1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *  http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.struts2.jasper.servlet;
20  
21  import java.io.FileNotFoundException;
22  import java.io.IOException;
23  import java.net.URL;
24  
25  import javax.servlet.Servlet;
26  import javax.servlet.ServletConfig;
27  import javax.servlet.ServletContext;
28  import javax.servlet.ServletException;
29  import javax.servlet.SingleThreadModel;
30  import javax.servlet.UnavailableException;
31  import javax.servlet.http.HttpServletRequest;
32  import javax.servlet.http.HttpServletResponse;
33  import javax.servlet.jsp.tagext.TagInfo;
34  
35  import org.apache.struts2.jasper.JasperException;
36  import org.apache.struts2.jasper.JspCompilationContext;
37  import org.apache.struts2.jasper.Options;
38  import org.apache.struts2.jasper.compiler.ErrorDispatcher;
39  import org.apache.struts2.jasper.compiler.JavacErrorDetail;
40  import org.apache.struts2.jasper.compiler.JspRuntimeContext;
41  import org.apache.struts2.jasper.compiler.Localizer;
42  import org.apache.struts2.jasper.runtime.JspSourceDependent;
43  import org.apache.struts2.jasper.runtime.InstanceHelper;
44  import org.apache.juli.logging.Log;
45  import org.apache.juli.logging.LogFactory;
46  import org.apache.tomcat.InstanceManager;
47  
48  /**
49   * The JSP engine (a.k.a Jasper).
50   *
51   * The servlet container is responsible for providing a
52   * URLClassLoader for the web application context Jasper
53   * is being used in. Jasper will try get the Tomcat
54   * ServletContext attribute for its ServletContext class
55   * loader, if that fails, it uses the parent class loader.
56   * In either case, it must be a URLClassLoader.
57   *
58   * @author Anil K. Vijendran
59   * @author Harish Prabandham
60   * @author Remy Maucherat
61   * @author Kin-man Chung
62   * @author Glenn Nielsen
63   * @author Tim Fennell
64   */
65  
66  public class JspServletWrapper {
67  
68      // Logger
69      private Log log = LogFactory.getLog(JspServletWrapper.class);
70  
71      private Servlet theServlet;
72      private String jspUri;
73      private Class servletClass;
74      private Class tagHandlerClass;
75      private JspCompilationContext ctxt;
76      private long available = 0L;
77      private ServletConfig config;
78      private Options options;
79      private boolean firstTime = true;
80      private boolean reload = true;
81      private boolean isTagFile;
82      private int tripCount;
83      private JasperException compileException;
84      private long servletClassLastModifiedTime;
85      private long lastModificationTest = 0L;
86  
87      /*
88       * JspServletWrapper for JSP pages.
89       */
90      public JspServletWrapper(ServletConfig config, Options options, String jspUri,
91                        boolean isErrorPage, JspRuntimeContext rctxt)
92              throws JasperException {
93  
94  	this.isTagFile = false;
95          this.config = config;
96          this.options = options;
97          this.jspUri = jspUri;
98          ctxt = new JspCompilationContext(jspUri, isErrorPage, options,
99  					 config.getServletContext(),
100 					 this, rctxt, null);
101     }
102 
103     /*
104      * JspServletWrapper for tag files.
105      */
106     public JspServletWrapper(ServletContext servletContext,
107 			     Options options,
108 			     String tagFilePath,
109 			     TagInfo tagInfo,
110 			     JspRuntimeContext rctxt,
111 			     URL tagFileJarUrl)
112 	    throws JasperException {
113 
114 	this.isTagFile = true;
115         this.config = null;	// not used
116         this.options = options;
117 	this.jspUri = tagFilePath;
118 	this.tripCount = 0;
119         ctxt = new JspCompilationContext(jspUri, tagInfo, options,
120 					 servletContext, this, rctxt,
121 					 tagFileJarUrl);
122     }
123 
124     public JspCompilationContext getJspEngineContext() {
125         return ctxt;
126     }
127 
128     public void setReload(boolean reload) {
129         this.reload = reload;
130     }
131 
132     public Servlet getServlet()
133         throws ServletException, IOException, FileNotFoundException
134     {
135         if (reload) {
136             synchronized (this) {
137                 // Synchronizing on jsw enables simultaneous loading
138                 // of different pages, but not the same page.
139                 if (reload) {
140                     // This is to maintain the original protocol.
141                     destroy();
142                     
143                     Servlet servlet = null;
144                     
145                     try {
146                         servletClass = ctxt.load();
147                         servlet = (Servlet) servletClass.newInstance();
148                         final InstanceManager instanceManager = InstanceHelper.getServletInstanceManager(config);
149                         if (instanceManager != null) {
150                             InstanceHelper.postConstruct(instanceManager, servlet);
151                         }
152                     } catch (IllegalAccessException e) {
153                         throw new JasperException(e);
154                     } catch (InstantiationException e) {
155                         throw new JasperException(e);
156                     } catch (Exception e) {
157                         throw new JasperException(e);
158                     }
159                     
160                     servlet.init(config);
161 
162                     if (!firstTime) {
163                         ctxt.getRuntimeContext().incrementJspReloadCount();
164                     }
165 
166                     theServlet = servlet;
167                     reload = false;
168                 }
169             }    
170         }
171         return theServlet;
172     }
173 
174     public ServletContext getServletContext() {
175         return config.getServletContext();
176     }
177 
178     /**
179      * Sets the compilation exception for this JspServletWrapper.
180      *
181      * @param je The compilation exception
182      */
183     public void setCompilationException(JasperException je) {
184         this.compileException = je;
185     }
186 
187     /**
188      * Sets the last-modified time of the servlet class file associated with
189      * this JspServletWrapper.
190      *
191      * @param lastModified Last-modified time of servlet class
192      */
193     public void setServletClassLastModifiedTime(long lastModified) {
194         if (this.servletClassLastModifiedTime < lastModified) {
195             synchronized (this) {
196                 if (this.servletClassLastModifiedTime < lastModified) {
197                     this.servletClassLastModifiedTime = lastModified;
198                     reload = true;
199                 }
200             }
201         }
202     }
203 
204     /**
205      * Compile (if needed) and load a tag file
206      */
207     public Class loadTagFile() throws JasperException {
208 
209         try {
210             if (ctxt.isRemoved()) {
211                 throw new FileNotFoundException(jspUri);
212             }
213             if (options.getDevelopment() || firstTime ) {
214                 synchronized (this) {
215                     firstTime = false;
216                     ctxt.compile();
217                 }
218             } else {
219                 if (compileException != null) {
220                     throw compileException;
221                 }
222             }
223 
224             if (reload) {
225                 tagHandlerClass = ctxt.load();
226                 reload = false;
227             }
228         } catch (FileNotFoundException ex) {
229             throw new JasperException(ex);
230 	}
231 
232 	return tagHandlerClass;
233     }
234 
235     /**
236      * Compile and load a prototype for the Tag file.  This is needed
237      * when compiling tag files with circular dependencies.  A prototpe
238      * (skeleton) with no dependencies on other other tag files is
239      * generated and compiled.
240      */
241     public Class loadTagFilePrototype() throws JasperException {
242 
243 	ctxt.setPrototypeMode(true);
244 	try {
245 	    return loadTagFile();
246 	} finally {
247 	    ctxt.setPrototypeMode(false);
248 	}
249     }
250 
251     /**
252      * Get a list of files that the current page has source dependency on.
253      */
254     public java.util.List getDependants() {
255         try {
256             Object target;
257             if (isTagFile) {
258                 if (reload) {
259                     tagHandlerClass = ctxt.load();
260                     reload = false;
261                 }
262                 target = tagHandlerClass.newInstance();
263             } else {
264                 target = getServlet();
265             }
266             if (target != null && target instanceof JspSourceDependent) {
267                 return ((java.util.List) ((JspSourceDependent) target).getDependants());
268             }
269         } catch (Throwable ex) {
270         }
271         return null;
272     }
273 
274     public boolean isTagFile() {
275 	return this.isTagFile;
276     }
277 
278     public int incTripCount() {
279 	return tripCount++;
280     }
281 
282     public int decTripCount() {
283 	return tripCount--;
284     }
285 
286     public void service(HttpServletRequest request, 
287                         HttpServletResponse response,
288                         boolean precompile)
289 	    throws ServletException, IOException, FileNotFoundException {
290         
291         try {
292 
293             if (ctxt.isRemoved()) {
294                 throw new FileNotFoundException(jspUri);
295             }
296 
297             if ((available > 0L) && (available < Long.MAX_VALUE)) {
298                 if (available > System.currentTimeMillis()) {
299                     response.setDateHeader("Retry-After", available);
300                     response.sendError
301                         (HttpServletResponse.SC_SERVICE_UNAVAILABLE,
302                          Localizer.getMessage("jsp.error.unavailable"));
303                     return;
304                 } else {
305                     // Wait period has expired. Reset.
306                     available = 0;
307                 }
308             }
309 
310             /*
311              * (1) Compile
312              */
313             if (options.getDevelopment() || firstTime ) {
314                 synchronized (this) {
315                     firstTime = false;
316 
317                     // The following sets reload to true, if necessary
318                     ctxt.compile();
319                 }
320             } else {
321                 if (compileException != null) {
322                     // Throw cached compilation exception
323                     throw compileException;
324                 }
325             }
326 
327             /*
328              * (2) (Re)load servlet class file
329              */
330             getServlet();
331 
332             // If a page is to be precompiled only, return.
333             if (precompile) {
334                 return;
335             }
336 
337         } catch (ServletException ex) {
338             if (options.getDevelopment()) {
339                 throw handleJspException(ex);
340             } else {
341                 throw ex;
342             }
343         } catch (IOException ex) {
344             if (options.getDevelopment()) {
345                 throw handleJspException(ex);
346             } else {
347                 throw ex;
348             }
349         } catch (IllegalStateException ex) {
350             if (options.getDevelopment()) {
351                 throw handleJspException(ex);
352             } else {
353                 throw ex;
354             }
355         } catch (Exception ex) {
356             if (options.getDevelopment()) {
357                 throw handleJspException(ex);
358             } else {
359                 throw new JasperException(ex);
360             }
361         }
362 
363         try {
364             
365             /*
366              * (3) Service request
367              */
368             if (theServlet instanceof SingleThreadModel) {
369                // sync on the wrapper so that the freshness
370                // of the page is determined right before servicing
371                synchronized (this) {
372                    theServlet.service(request, response);
373                 }
374             } else {
375                 theServlet.service(request, response);
376             }
377 
378         } catch (UnavailableException ex) {
379             String includeRequestUri = (String)
380                 request.getAttribute("javax.servlet.include.request_uri");
381             if (includeRequestUri != null) {
382                 // This file was included. Throw an exception as
383                 // a response.sendError() will be ignored by the
384                 // servlet engine.
385                 throw ex;
386             } else {
387                 int unavailableSeconds = ex.getUnavailableSeconds();
388                 if (unavailableSeconds <= 0) {
389                     unavailableSeconds = 60;        // Arbitrary default
390                 }
391                 available = System.currentTimeMillis() +
392                     (unavailableSeconds * 1000L);
393                 response.sendError
394                     (HttpServletResponse.SC_SERVICE_UNAVAILABLE, 
395                      ex.getMessage());
396             }
397         } catch (ServletException ex) {
398             if(options.getDevelopment()) {
399                 throw handleJspException(ex);
400             } else {
401                 throw ex;
402             }
403         } catch (IOException ex) {
404             if(options.getDevelopment()) {
405                 throw handleJspException(ex);
406             } else {
407                 throw ex;
408             }
409         } catch (IllegalStateException ex) {
410             if(options.getDevelopment()) {
411                 throw handleJspException(ex);
412             } else {
413                 throw ex;
414             }
415         } catch (Exception ex) {
416             if(options.getDevelopment()) {
417                 throw handleJspException(ex);
418             } else {
419                 throw new JasperException(ex);
420             }
421         }
422     }
423 
424     public void destroy() {
425         if (theServlet != null) {
426             theServlet.destroy();
427             final InstanceManager instanceManager = InstanceHelper.getServletInstanceManager(config);
428             if (instanceManager != null) {
429                 try {
430                     InstanceHelper.preDestroy(instanceManager, theServlet);
431                 } catch (Exception e) {
432                     // Log any exception, since it can't be passed along
433                     log.error(Localizer.getMessage("jsp.error.file.not.found",
434                            e.getMessage()), e);
435                 }
436             }
437         }
438     }
439 
440     /**
441      * @return Returns the lastModificationTest.
442      */
443     public long getLastModificationTest() {
444         return lastModificationTest;
445     }
446     /**
447      * @param lastModificationTest The lastModificationTest to set.
448      */
449     public void setLastModificationTest(long lastModificationTest) {
450         this.lastModificationTest = lastModificationTest;
451     }
452 
453     /**
454      * <p>Attempts to construct a JasperException that contains helpful information
455      * about what went wrong. Uses the JSP compiler system to translate the line
456      * number in the generated servlet that originated the exception to a line
457      * number in the JSP.  Then constructs an exception containing that
458      * information, and a snippet of the JSP to help debugging.
459      * Please see http://issues.apache.org/bugzilla/show_bug.cgi?id=37062 and
460      * http://www.tfenne.com/jasper/ for more details.
461      *</p>
462      *
463      * @param ex the exception that was the cause of the problem.
464      * @return a JasperException with more detailed information
465      */
466     protected JasperException handleJspException(Exception ex) {
467         try {
468             Throwable realException = ex;
469             if (ex instanceof ServletException) {
470                 realException = ((ServletException) ex).getRootCause();
471             }
472 
473             // First identify the stack frame in the trace that represents the JSP
474             StackTraceElement[] frames = realException.getStackTrace();
475             StackTraceElement jspFrame = null;
476 
477             for (int i=0; i<frames.length; ++i) {
478                 if ( frames[i].getClassName().equals(this.getServlet().getClass().getName()) ) {
479                     jspFrame = frames[i];
480                     break;
481                 }
482             }
483 
484             if (jspFrame == null) {
485                 // If we couldn't find a frame in the stack trace corresponding
486                 // to the generated servlet class, we can't really add anything
487                 return new JasperException(ex);
488             }
489             else {
490                 int javaLineNumber = jspFrame.getLineNumber();
491                 JavacErrorDetail detail = ErrorDispatcher.createJavacError(
492                         jspFrame.getMethodName(),
493                         this.ctxt.getCompiler().getPageNodes(),
494                         null,
495                         javaLineNumber,
496                         ctxt);
497 
498                 // If the line number is less than one we couldn't find out
499                 // where in the JSP things went wrong
500                 int jspLineNumber = detail.getJspBeginLineNumber();
501                 if (jspLineNumber < 1) {
502                     throw new JasperException(ex);
503                 }
504 
505                 if (options.getDisplaySourceFragment()) {
506                     return new JasperException(Localizer.getMessage
507                             ("jsp.exception", detail.getJspFileName(),
508                                     "" + jspLineNumber) +
509                                     "\n\n" + detail.getJspExtract() +
510                                     "\n\nStacktrace:", ex);
511                     
512                 } else {
513                     return new JasperException(Localizer.getMessage
514                             ("jsp.exception", detail.getJspFileName(),
515                                     "" + jspLineNumber), ex);
516                 }
517             }
518         } catch (Exception je) {
519             // If anything goes wrong, just revert to the original behaviour
520             if (ex instanceof JasperException) {
521                 return (JasperException) ex;
522             } else {
523                 return new JasperException(ex);
524             }
525         }
526     }
527 
528 }